import signal
import syslog
import threading
import time
from abc import abstractmethod
from collections import deque
from datetime import datetime

DHCP_SERVER_IPV4_LEASE = "DHCP_SERVER_IPV4_LEASE"
KEA_LEASE_FILE_PATH = "/tmp/kea-lease.csv"
DEFAULE_LEASE_UPDATE_INTERVAL = 2  # unit: sec


class LeaseManager(object):
    def __init__(self, db_connector, kea_lease_file=KEA_LEASE_FILE_PATH):
        self.lease_handlers = [KeaDhcp4LeaseHandler(db_connector, kea_lease_file)]

    def start(self):
        """
        Register lease hanlder
        """
        for handler in self.lease_handlers:
            handler.register()


class LeaseHanlder(object):
    def __init__(self, db_connector, lease_update_interval=DEFAULE_LEASE_UPDATE_INTERVAL):
        self.db_connector = db_connector
        self.lease_update_interval = lease_update_interval
        self.last_update_time = None
        self.lock = threading.Lock()

    @abstractmethod
    def _read(self):
        """
        Read lease file to get newest lease information
        """
        raise NotImplementedError

    @abstractmethod
    def register(self):
        """
        Register callback function
        """
        raise NotImplementedError

    def update_lease(self):
        """
        Update lease table in STATE_DB
        """
        last_update_time = self.last_update_time
        curr_time = datetime.now()
        # If the time since the last update is less than self.lease_update_interval, then wait for a
        # self.lease_update_interval
        if last_update_time is not None and (curr_time - last_update_time).seconds < self.lease_update_interval:
            time.sleep(self.lease_update_interval)
            if self.last_update_time != last_update_time:
                # Means lease has been updated during sleep, no need to update this lease
                return
        if not self.lock.acquire(False):
            return
        new_lease = self._read()
        # Store old lease key
        old_lease_table = self.db_connector.get_state_db_table(DHCP_SERVER_IPV4_LEASE)
        old_lease_key = set(old_lease_table.keys())

        # 1.1 If start time equal to end time or lease expired, means lease has been released
        #     1.1.1 If current lease table has this old lease, delete it
        #     1.1.2 Else skip
        # 1.2 Else, means lease valid, save it.
        for key, value in new_lease.items():
            unix_time = datetime.now().timestamp()
            if value["lease_start"] == value["lease_end"] or unix_time >= int(value["lease_end"]):
                if key in old_lease_key:
                    self.db_connector.state_db.delete("{}|{}".format(DHCP_SERVER_IPV4_LEASE, key))
                continue
            new_key = "{}|{}".format(DHCP_SERVER_IPV4_LEASE, key)
            for k, v in new_lease[key].items():
                self.db_connector.state_db.hset(new_key, k, v)
        # Delete old lease not in new lease set
        for key in old_lease_key:
            if key not in new_lease.keys():
                # Delete entry
                self.db_connector.state_db.delete("{}|{}".format(DHCP_SERVER_IPV4_LEASE, key))
        self.last_update_time = datetime.now()
        self.lock.release()


class KeaDhcp4LeaseHandler(LeaseHanlder):
    def __init__(self, db_connector, lease_file=KEA_LEASE_FILE_PATH):
        LeaseHanlder.__init__(self, db_connector)
        self.lease_file = lease_file

    def register(self):
        """
        Register callback function of signal
        """
        signal.signal(signal.SIGUSR1, self._update_lease)

    def _read(self):
        # Read lease file generated by kea-dhcp4
        try:
            with open(self.lease_file, "r", encoding="utf-8") as fb:
                dq = deque(fb)
        except FileNotFoundError as err:
            syslog.syslog(syslog.LOG_ERR, "Cannot find lease file: {}".format(self.lease_file))
            raise err

        fdb_info = self._get_fdb_info()
        new_lease = {}
        # Get newest lease information of each client
        while dq:
            last_row = dq.pop()
            splits = last_row.split(",")
            # Skip header
            if splits[0] == "address":
                break
            ip_str = splits[0]
            mac_address = splits[1]
            valid_lifetime = splits[3]
            lease_end = splits[4]

            if mac_address not in fdb_info:
                syslog.syslog(syslog.LOG_WARNING, "Cannot not find {} in fdb table".format(mac_address))
                continue
            new_key = "{}|{}".format(fdb_info[mac_address], mac_address)
            if new_key in new_lease:
                continue
            new_lease[new_key] = {
                "lease_start": str(int(lease_end) - int(valid_lifetime)),
                "lease_end": lease_end,
                "ip": ip_str
            }
        return new_lease

    def _get_fdb_info(self):
        """
        Get fdb information, indicate that mac address comes from which dhcp interface.
        Returns:
            Dict of fdb information, sample:
            {
                "aa:bb:cc:dd:ee:ff": "Vlan1000"
            }
        """
        fdb_table = self.db_connector.get_state_db_table("FDB_TABLE")
        ret = {}
        for key in fdb_table.keys():
            splits = key.split(":", 1)
            ret[splits[1]] = splits[0]
        return ret

    def _update_lease(self, signum, frame):
        self.update_lease()
